接續上一篇的故事,阿明跟小美又經過了幾次的對話與討論,在便利貼專案中整理歸納了下列這幾個關鍵字:
有了這幾個關鍵字之後,所有的專案成員便一致認同該進行架構設計的階段了,但是重新設計架構不是一蹴可幾,分幾個不同的階段慢慢來才是更好的做法。首先第一步是為最完整的需求勾勒出大致上的輪廓,確保元件之間的交互關係之後,再選擇該架構中實作的優先順序,其中以滿足之前的需求為優先,在確定不會破壞任何行為之後,再將新需求實作進來,最後做成完成品。
下圖是這幾個元件之間的交互關係,這不是類別圖,也不是流程圖,就只是表達模型的一張圖。
Gesture
可以控制 CoEditor
CoEditor
擁有 ViewPort
, ContextMenu
, AdderButton
ViewPort
擁有許多的 StickyNote
接著我們試看看之前訂出來的 Use case 在這樣的架構下能不能正確運行吧:
以這個 Use case 來說,是相對難實現的,因為手勢操作會從 Gesture 進來,經過 CoEditor、ViewPort,最後才能直接操作到對應的 StickyNote,而且“有沒有接觸到便利貼”這件事變成要從 Domain 這邊來處理,也就是說要依據 StickyNote 的寬跟高還有位置來判斷點擊事件,做的事情變得有點多,但這其實也隱含了一件事,就是 StickyNote 的寬跟高應該是 Domain 中的一部分,但是到現在都還沒有討論到這一塊。為了避免將專案規模越弄越大,這時候最應該要做的事情是將此事與 Stakeholder 討論,一方面思考較簡單的解法。
經過討論與思考後,因為實作的工程浩大,最終決定使用原本的做法:從外面的 StickyNoteView 操控相對應的便利貼,如果之後有需要的話會再統一由 Gesture 控制。
既然上面都決定了拖曳便利貼行為還是交給每個便利貼自己處理,選擇行為也會如法炮製,因此 StickyNote
將會有一個點擊行為的 Callback 經由 ViewPort
再交給 CoEditor
處理,CoEditor
將會知道該讓哪一個便利貼啟用選擇狀態,並將這選擇狀態再傳遞回去給所有的便利貼,以更新選擇狀態。
與此同時,CoEditor
開啟了 ContextMenu
,當按下刪除時,由於 CoEditor
已經知道選擇的便利貼是哪一個,所以刪除就變得簡單了。
跟刪除的 Use case 是差不多的,所以就不分析了
這邊預計是由 ViewPort
來控制的,當可見範圍改變時,可能是執行其中的 move() 或是 zoomIn() 之類的函式,然後可以看到的便利貼數量也會因此增加或減少,因此這元件應該要直接擁有 NoteRepostory
的相依才是。
這也是不好解決的問題,但是根據之前在 Clean architecture 分析時學到的,可以將這邊的 StickyNote 改成 StickyNoteId 就好,等到該便利貼被 ViewPort
觀察到時,再依序建立出 StickyNoteView 還有 StickyNoteViewModel ,最後 StickyNoteViewModel 會與這邊的 StickyNote 產生連結而拿到最新的值。
從上一個階段我們可以得知,其中最核心的元件是 CoEditor
,於是我想採取的第一步是從建立這個類別開始,慢慢把 ViewModel 的實作搬到核心元件中,當這部分成功了之後,就可以再針對 Domain 中各小元件來進行拆分。於是就產生了以下的類別圖:
大概解釋一下這張圖:藍色的部分是 Domain ,紅色的部分是 View ,黑色的部分是 Data 。
首先可以來看看 Domain 的部分,總共有三個不同的元件:CoEditor
、ContextMenu
與 NoteRepository
,他們各別的職責就算不用解釋也大概知道在做什麼,至於其他的部分,像是 Gesture 與 ViewPort ,就留給之後再做,目前這樣的規模要動的程式碼已經很多了。
Data 這邊沒有任何變化,所以就不解說了,但是 View 的變化就很大了,除了原本的 EditorViewModel 將重新命名為 CoEditorViewModel
之外,還有其他兩個新的 ViewModel:NoteViewModel
與 ContextMenuViewModel
,ViewModel 的定位將會從架構中的核心,轉移到 Clean architecture 中的 Port and Adapter,因此將不會預期這些元件會有複雜的領域知識與邏輯,就只是一個 Adapter 而已。其中也可以注意一下箭頭的方向,他們都有符合 Dependency rule,箭頭是從外層內層。至於其他的,剩下來的就是 Jetpack Compose 實作的 View 層元件:CoEditorScreen
、ViewPortView
、NoteView
、ContextMenuView
,他們的關係並不複雜,但是其中有一個最大的變化就是,現在不只有 CoEditorScreen
認識 ViewModel ,其他的 View 各認識他們所應該認識的 ViewModel ,這代表 ContextMenuView
, NoteView
這些 View 將會是 Stateful 的,在之前的篇幅中有說過,Stateful 元件的可重用性不高,因此是比較沒有那麼推薦去做一個有 Stateful 的 Composable function,至於這邊要怎麼處理,在之後將會詳細的介紹。
從第 22 天開始,我們做了很多架構的討論以及設計,當中也產出了一些手繪的架構圖,從比較宏觀的角度將所有需要的元件寫出來,這種設計的方式,叫做 Top-down design,一開始在學習 TDD ,或是一些敏捷開發理論時,會以為這樣的開發方式很“不敏捷”,因為必須經過很長的一段時間設計才能得到成果,看起來很像是在做瀑布式開發。但後來發現,這樣的設計方式其實跟敏捷一點都不衝突,因為在做“全面”的設計時,並不代表這樣的設計是一次到位,不允許被改變的,相對的,如果好好運用敏捷的方式,在經過數個開發循環之後,你將會發現一開始設計的架構還有很多地方需要改進的,這時就是最好的修正機會。然後再進行討論,微調架構,在下一個循環修正完之後,你會發現你對正在解決的問題有更深層的理解,團隊有了更多的共識。